package com.huisenky.smallidea.common.cal;

import java.math.BigDecimal;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 表达式计算
 * 根据现有表达式计算，目前符号只支持+-*\/()
 */
public class BeCal {

    private BeCal() {
    }

    /**
     * 根据表达式计算
     *
     * @param expr 表达式 例如：((0.1+0.2*0.3+0.5)/0.4)*0.2
     * @return 计算值
     * @throws Exception 计算错误
     */
    public static String cal(String expr) throws Exception {
        // 数字栈
        Stack<BigDecimal> number = new Stack<>();
        // 符号栈
        Stack<String> operator = new Stack<>();
        // 在栈顶压人一个null，配合它的优先级，目的是减少下面程序的判断
        operator.push(null);

        // 将expr打散为运算数和运算符
        // 这个正则为匹配表达式中的数字或运算符
        Pattern p = Pattern.compile("(?<!\\d)-?\\d+(\\.\\d+)?|[+\\-*/()]");
        Matcher m = p.matcher(expr);
        while (m.find()) {
            String temp = m.group();
            // 遇到符号
            if (temp.matches("[+\\-*/()]")) {
                // 遇到左括号，直接入符号栈
                if (temp.equals("(")) {
                    operator.push(temp);
                } else if (temp.equals(")")) {
                    // 遇到右括号，"符号栈弹栈取栈顶符号b，数字栈弹栈取栈顶数字a1，数字栈弹栈取栈顶数字a2，计算a2 b a1 ,将结果压入数字栈"，重复引号步骤至取栈顶为左括号，将左括号弹出
                    String b = null;
                    while (!(b = operator.pop()).equals("(")) {
                        BigDecimal a1 = number.pop();
                        BigDecimal a2 = number.pop();
                        number.push(BigDecimalCal(a2, a1, b.charAt(0)));
                    }
                } else {
                    // 遇到运算符，满足该运算符的优先级大于栈顶元素的优先级压栈；否则计算后压栈
                    while (getPriority(temp) <= getPriority(operator.peek())) {
                        BigDecimal a1 = number.pop();
                        BigDecimal a2 = number.pop();
                        String b = operator.pop();
                        number.push(BigDecimalCal(a2, a1, b.charAt(0)));
                    }
                    operator.push(temp);
                }
            } else {
                //遇到数字，直接压入数字栈
                number.push(new BigDecimal(temp));
            }
        }

        // 遍历结束后，符号栈数字栈依次弹栈计算，并将结果压入数字栈
        while (operator.peek() != null) {
            BigDecimal a1 = number.pop();
            BigDecimal a2 = number.pop();
            String b = operator.pop();
            number.push(BigDecimalCal(a2, a1, b.charAt(0)));
        }
        //科学计数，保留2位小数
        BigDecimal pop = number.pop().setScale(2, BigDecimal.ROUND_HALF_UP);
        //数值完全显示
        return pop.toPlainString();
    }

    /**
     * 单运算符计算
     *
     * @param a1       数字
     * @param a2       数字
     * @param operator 表达式
     * @return 计算值
     * @throws Exception 计算错误
     */
    private static BigDecimal BigDecimalCal(BigDecimal a1, BigDecimal a2, char operator) throws Exception {
        switch (operator) {
            case '+':
                return a1.add(a2);
            case '-':
                return a1.subtract(a2);
            case '*':
                return a1.multiply(a2);
            case '/':
                return a1.divide(a2, 20, BigDecimal.ROUND_HALF_UP);
            default:
                break;
        }
        throw new Exception("操作符暂不支持");
    }

    /**
     * 表达式中符号的优先级
     * 运算符号数字越小，优先级越低
     * 括号优先级低是因为需要先计算括号中的表达式
     *
     * @param s 符号
     * @return 符号优先级0最低
     * @throws Exception 优先级获取错误
     */
    private static int getPriority(String s) throws Exception {
        if (s == null) return 0;
        else if (s.equals("(")) return 1;
        else if (s.equals("+") || s.equals("-")) return 2;
        else if (s.equals("*") || s.equals("/")) return 3;

        throw new Exception("符号暂不支持");
    }
}

